



## 项目和子项目的泛化

我先来回顾一下需求：我们把业务由“零零后公司”扩展到了“九零后公司”，而“九零后公司”会把项目进一步分为子项目，然后在子项目上报工时。

先看看目前的模型是什么样
<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241211204341.png"/>

根据需求，第一步，先在模型里增加子项目。

<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241211204426.png"/>

上图说明，一个项目可以没有子项目，也可以有多个子项目。不过，现在这张图里还没有说明可以在子项目上报工时。


于是，在工时记录和子项目之间增加了一个一对多的关联。

<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241211204528.png"/>

增加了这个关联以后，确实可以说明在子项目上能登记工时了。但是我发现，在项目和子项目一端，两个多重性都是“1..1”，这似乎有问题呀？

就拿项目和工时记录的关联来说，“1..1”的后一个 1 没有问题，但前一个 1 说明了，一条工时记录必然关联一个项目，这就不对了。因为，如果有的工时记录是报到子项目上的，那么就不会对应一个项目了。所以应该改为“0..1”才对。同样的道理，子项目一端也应该改为“0..1”。



于是，把图改成下面这样。

<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241211204738.png"/>

现在的模型确实解决了刚才的问题。但是你很快又发现一个新问题。现在的模型意味着，一条工时记录可以既不关联一个项目，也不关联一个子项目；另一方面，一条工时记录可以既关联一个项目，也关联一个子项目。这显然是不对的。

正确的逻辑应该是：<RedSpan>一条工时记录要么关联一个项目，要么关联一个子项目，但不能同时关联两者，也不能两者都不关联</RedSpan>。运用目前我们已经掌握的建模技术，只能用注释的形式增加一个约束。


于是，你把模型改成下面这样。

<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241211210759.png"/>

一般来说，如果能够用 UML 里自带的建模符号，比如说类、关联等等，那么应该优先使用建模符号，而利用注释和约束，其实是一种“补救”措施。么，在 UML 里有没有一个符号，可以帮助我们表达这种约束呢？其实是有的，这就是我们今天想介绍的主角——“泛化”。


在正式引入这个符号之前，我先从另一个角度捋一下这个逻辑：项目和子项目都是可以报工时的“东西”。而一条工时记录，必须要关联到一个这种“东西”上。


你问我：“那么这个‘东西’应该叫什么呢？”我说：“就叫‘工时项’吧。”有了这个名词以后，我们可以把这个逻辑再说一遍：<RedSpan>项目和子项目统称为“工时项”，一个工时项可以关联 0 到多条工时记录，一条工时记录必须关联且仅关联一个工时项。</RedSpan>


根据这个表述，我接过键盘，通过一个新的符号，把模型改成了下面的样子。

<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241211211428.png"/>

图里面的空心三角，就表示前面说的“统称”的关系，由于工时项的含义比起项目和子项目要更广泛，所以这种关系在 UML 里叫做“泛化”（generalization）。

这时候，<RedSpan>工时项称为项目和子项目的“父类”，而项目和子项目称为工时项的子类</RedSpan>。

:::tip
如果 A 类和 B 类可以统称为 C 类的话 ,C 类和 A 、B 两个类就具有泛化关系，其中， C 是父类，A 和 B 是子类。泛化关系用一个空心箭头表示，由子类指向父类。
:::

除了统称以外，泛化关系转换成自然语言，还可以有另外三种说法。（分类、is-a、共性）。
* 第一种说法是，工时项分成两类，一类是项目，另一类是子项目。也就是说，泛化表示的是一种“分类”关系。例如生物可以分成动物和植物，动物又可以分成哺乳动物和爬行动物等等。
* 第二种说法是，一个项目是一个工时项，一个子项目也是一个工时项。也就是所谓“是一个”（is-a）的关系。
* 第三种说法是，项目和子项目具有“共性”，也就是都能报工时。我们把这个共性的概念提取出来，称为工时项。另一方面，项目和子项目又具有“个性”，也就是两者有差别，比如说，要为项目分配项目经理，而子项目则不需要。

统称、分类、是一个，以及“共性/个性”这四种说法，虽然从表面上看不同，背后的含义却是完全一样的。在领域模型里，不论那种说法，都可以用泛化来表达。总的来说，<RedSpan>泛化是一种强大的抽象机制，能够同时表现出不同对象间的共性和个性。</RedSpan>


引入了工时项以后，你还注意到了模型图里其他几个变化。


首先，原来工时记录和项目还有子项目之间的两条关联不见了，取而代之的是，工时记录和工时项之间的一条一对多关联。这是因为，父类的属性、规则、关联等等要素，都可以被子类继承。所以，工时项和工时记录之间有一对多的关联，也就意味着工时项的子类，项目和子项目，都和工时记录有这种关联，所以就不需要重复了，重复画的话反而是错的。

其次，工时项一端的多重性变回了“1..1”，这也就表达了前面说的 <RedSpan>一条工时记录必须关联且仅关联一个工时项这一业务规则</RedSpan>。

最后，我们刚刚为工时记录添加的那条约束被删除了，这是因为修改过的模型本身已经表达了这个约束。

## 普通工时项
通过引入泛化，我们解决了在子项目上报工时的需求。现在，我接着说说来自另一家企业“八零后公司”的需求。他们既可以按项目，也可以按子项目报工时，这些我们已经基本解决了。


但是，他们还有一些活动和具体项目无关，比如说：员工在项目不紧张的时候，可以利用空余时间学习；或者，管理者常常花大量时间在管理和沟通上，其中有一些时间很难归入某一个具体的项目。而这家公司希望也能为这些不属于任何项目的活动报工时。


你的第一个想法是：借用项目的概念。也就是说，建几个特殊的项目，就叫“学习时间”“管理时间”等等。然后大家就可以像普通项目那样报工时了。


但我对这个思路提出了质疑：“学习时间和我们一般人理解的项目，是两个不同的概念。如果我们借用了项目来表达学习时间，虽然在短期内解决了问题，但如果开了这个口子，那么混淆的概念就会越来越多，系统就会越来越难以理解和维护。

事实上，直接“借用”系统中已经存在的机制，在短期内虽然达到了目的，但长期来看会导致概念混乱，这种做法是很多开发团队常见的错误。而错误的根源，就在于我们没有掌握一种优雅的方法，来处理不同概念的共性和个性。学习时间和项目，既然都可以报工时，那么两者必然存在着共性。现在你应该已经想到了，这个共性就是前面已经抽象出来的工时项。

于是你又问了我一个问题：“学习时间和管理时间，以及其他不算作项目的时间，有没有一个统一的称呼呢？”我说：“就叫普通工时项吧。”基于这个理解，你画出了下面的模型图。




<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241211212919.png"/>


为工时项增加了一个普通工时项的子类。同时，把工时记录的约束做了相应的修改，变成了对于项目和子项目，员工只能在被分配的项目上报工时，以及，对于项目和子项目，只有在项目有效期内才能报工时。


普通工时项和项目管理已经没有关系了，还把它放在项目管理模块里，这合适吗？”你也感觉不太合适了。由于普通工时项是用于工时管理的，所以你把普通工时项移到了工时管理模块。

<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241211213322.png"/>


紧跟着我又问了一个问题：“工时项还应该留在项目管理模块吗？”你思考了一下，说：“工时项应该是服务于工时管理的，而不是项目管理的关注点，所以，工时项也应该移入工时管理。”我们对此达成了共识，于是你把模型改成下面的样子。


<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241211213415.png"/>

## 隐式概念的显式化

像“工时项”“普通工时项”“项目工时粒度”这些概念，在原来的业务术语里是没有的。在我们的抽象过程中才被发掘出来。

这其实是领域建模中的一种常见的现象。一些概念本来就隐含在业务逻辑内部，但没有经过抽象的话，往往不会感觉到，更不会有人来命名。

而一旦采用领域建模方法对领域知识进行抽象，这些概念就会暴露出来。这时候，开发人员和业务人员就要一起对这个抽象达成共识，并且给新发现的概念起一个恰当的名字，并把这个新词加入统一语言。这样，我们对领域知识的理解就又深化了一层。这就是《DDD》原书第 9 章中提到过的“隐式概念的显式化”。


## 总结
泛化是一种分类机制，能够对领域对象的共性和个性进行抽象。泛化用一个空心箭头表示。箭头由子类指向的是父类。子类会继承父类的属性、关联和逻辑。



假定 C 是父类，A 和 B 是它的子类，那么对应到自然语言，可以有四种说法。
* A 和 B 统称为 C ，例如，甜粽子和咸粽子统称为粽子。
* C 可以分为 A 和 B 两类，例如，粽子可以分为咸粽子和甜粽子两类。
* 一个 A 是一个 C ，一个 B 也是一个 C，例如，一个甜粽子是一个粽子，一个咸粽子也是一个粽子。
* A 和 B 具有共性，表示共性的概念称为 C ，例如，甜粽子和咸粽子具有共性，表示共性的概念称为粽子。

在领域建模的过程中，还经常遇到隐式概念的显式化，从而引入新的词汇，丰富了统一语言，也深化了对领域知识的理解。




